<?php

namespace Hiders\WebmanCrud\Command;

use Exception;
use Illuminate\Support\Str;
use support\Db;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class CrudCommand extends Command
{
    protected static $defaultName = 'hiders:crud';
    protected static $defaultDescription = 'Generate models and admin controllers';

    protected array $tmplList = [];
    protected array $internalKeywords = [
        'abstract',
        'and',
        'array',
        'as',
        'break',
        'callable',
        'case',
        'catch',
        'class',
        'clone',
        'const',
        'continue',
        'declare',
        'default',
        'die',
        'do',
        'echo',
        'else',
        'elseif',
        'empty',
        'enddeclare',
        'endfor',
        'endforeach',
        'endif',
        'endswitch',
        'endwhile',
        'eval',
        'exit',
        'extends',
        'final',
        'for',
        'foreach',
        'function',
        'global',
        'goto',
        'if',
        'implements',
        'include',
        'include_once',
        'instanceof',
        'insteadof',
        'interface',
        'isset',
        'list',
        'namespace',
        'new',
        'or',
        'print',
        'private',
        'protected',
        'public',
        'require',
        'require_once',
        'return',
        'static',
        'switch',
        'throw',
        'trait',
        'try',
        'unset',
        'use',
        'var',
        'while',
        'xor',
    ];

    private array $dataTypeList = [
        'mysql' => [
            'tinyint'    => 'int',
            'smallint'   => 'int',
            'mediumint'  => 'int',
            'bigint'     => 'int',
            'decimal'    => 'string',
            'double'     => 'string',
            'float'      => 'string',
            'longtext'   => 'string',
            'text'       => 'string',
            'mediumtext' => 'string',
            'smalltext'  => 'string',
            'tinytext'   => 'string',
            'varchar'    => 'string',
            'char'       => 'string',
            'set'        => 'string',
            'enum'       => 'string',
            'year'       => 'string',
            'date'       => 'string',
            'time'       => 'string',
            'datetime'   => 'string',
            'timestamp'  => 'string',
        ],
    ];

    private array $createTimeDict = ['created_at', 'create_time', 'createtime'];
    private array $updateTimeDict = ['updated_at', 'update_time', 'updatetime'];
    private array $deleteTimeDict = ['deleted_at', 'delete_time', 'deletetime'];

    /**
     * @return void
     */
    protected function configure(): void
    {
        $this->addArgument('table name', InputArgument::REQUIRED, 'Table name to generate CRUD')
             ->addArgument('database connection', InputArgument::OPTIONAL, 'database connection name')
             ->addOption('controller', 'c', InputOption::VALUE_REQUIRED, 'controller name')
             ->addOption('model', 'm', InputOption::VALUE_REQUIRED, 'model name')
             ->addOption('relation', 'r', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'relation model')
             ->addOption('force', 'f', InputOption::VALUE_NONE, '<fg=red>Force</> overwrite');
    }

    /**
     * @param InputInterface  $input
     * @param OutputInterface $output
     * @return int
     * @throws \Exception
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $table = $input->getArgument('table name');
        $connection = $input->getArgument('database connection');
        $controller = $input->getOption('controller');
        $model = $input->getOption('model') ?: $controller;
        $force = (bool)$input->getOption('force');
        $relation = $input->getOption('relation') ?: [];

        if (!$table) {
            throw new Exception('表名称不能为空');
        }

        $controllerDir = config('plugin.hiders.webman-crud.app.controller_dir');
        $modelDir = config('plugin.hiders.webman-crud.app.model_dir');

        $tablePrefix = Db::connection($connection)->getTablePrefix();

        $modelTableInfo = Db::connection($connection)->select("SHOW TABLE STATUS LIKE '{$tablePrefix}{$table}'");
        if (empty($modelTableInfo)) {
            throw new Exception("找不到数据表【{$table}】");
        }
        $modelTableInfo = $modelTableInfo[0];

        //控制器
        [
            $controllerNamespace,
            $controllerName,
            $controllerFile,
            $controllerArr,
        ] = $this->getControllerData($controllerDir, $table, $controller);
        //模型
        [$modelNamespace, $modelName, $modelFile, $modelArr] = $this->getModelData($modelDir, $table, $model);

        $controllerUse = [
            // 'use DI\Annotation\Inject;',
            'use Hiders\WebmanCrud\Trait\Crud;',
            'use ' . (config('plugin.hiders.webman-crud.app.base_controller') ?: 'Hiders\WebmanCrud\Base\Controller') . ';',
            'use ' . (config('plugin.hiders.webman-crud.app.base_model') ?: 'Hiders\WebmanCrud\Base\Model') . ';',
        ];
        $modelUse = ['use ' . (config('plugin.hiders.webman-crud.app.base_model') ?: 'Hiders\WebmanCrud\Base\Model') . ';'];

        $controllerExtends = array_slice(explode('\\', config('plugin.hiders.webman-crud.app.base_controller') ?: 'Hiders\WebmanCrud\Base\Controller'), -1)[0];
        $modelExtends = array_slice(explode('\\', config('plugin.hiders.webman-crud.app.base_model') ?: 'Hiders\WebmanCrud\Base\Model'), -1)[0];

        //构建关联模型参数
        $modelRelationFieldAppends = [];
        $modelRelationField = [];
        $modelRelationTable = [];
        $controllerRelation = [];
        if (!empty($relation)) {
            foreach ($relation as $v) {
                [$rTable, $rFk, $rLk, $rColumns, $rMode] = array_merge(explode('/', $v), [
                    null,
                    null,
                    null,
                    null,
                ]);
                // $rModelTableInfo = Db::select("SHOW TABLE STATUS LIKE ?", [$rTable]);
                // if (empty($rModelTableInfo)) {
                //     throw new Exception("找不到数据表【{$rTable}】");
                // }

                // 处理参数默认值
                if (empty($rFk)) {
                    $rFk = Db::connection($connection)
                             ->select("SELECT COLUMN_NAME FROM `information_schema`.`COLUMNS` WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_KEY = 'PRI'", [
                                 Db::connection($connection)->getDatabaseName(),
                                 $tablePrefix . $rTable,
                             ]);
                    if (empty($rFk) || count($rFk) > 1) {
                        throw new Exception('获取关联表主键失败:' . $rTable . var_export($rFk, true));
                    }
                    $rFk = $rFk[0]->COLUMN_NAME;
                }
                $rLk = $rLk ?: $rTable . '_id';
                $rColumns = empty($rColumns) || $rColumns === 'all' ? [] : explode('+', $rColumns);
                $rMode = empty($rMode) && count($rColumns) === 1 ? 'field' : match (strtolower($rMode ?? '')) {
                    'belongsto' => 'belongsTo',
                    'hasmany' => 'hasMany',
                    'field' => 'field',
                    default => 'hasOne'
                };

                // 获取关联模型namespace
                [
                    $rModelNamespace,
                    $rModelName,
                    $rModelFile,
                    $rModelArr,
                ] = $this->getModelData($modelDir, $rTable);
                //todo 关联模型自定义后找不到
                if ($modelNamespace != $rModelNamespace && file_exists($rModelFile)) {
                    $modelUse[] = "use {$rModelNamespace}\{$rModelName};";
                }

                // 构建关联模型模板参数
                if ($rMode == 'field') {
                    $modelUse[] = 'use Illuminate\Database\Eloquent\Casts\Attribute;';
                    foreach ($rColumns as $vv) {
                        //判断是否自定义关联名
                        if (str_contains($vv, '*')) {
                            $vv = explode('*', $vv);
                            $relationField = $vv[1];
                            $vv = $vv[0];
                        } else {
                            $relationField = $rTable . '_' . $vv;
                        }

                        //构建模版变量
                        $modelRelationFieldAppends[] = $relationField;
                        $modelRelationField[] = $this->getReplacedTmpl('mixins/modelRelationField', [
                            'modelRelationName'   => Str::camel($relationField),
                            'modelRelationModel'  => $rModelName,
                            'modelRelationFk'     => $rFk,
                            'modelRelationLk'     => $rLk,
                            'modelRelationColumn' => $vv,
                        ]);
                    }
                } else {
                    if (!empty($rColumns) && !in_array($rFk, $rColumns)) {
                        array_unshift($rColumns, $rFk);
                    }
                    $modelUse[] = 'use Illuminate\Database\Eloquent\Relations\\' . Str::ucfirst($rMode) . ';';
                    $modelRelationTable[] = $this->getReplacedTmpl('mixins/modelRelationTable', [
                        'modelRelationName'      => Str::camel($rTable),
                        'modelRelationModeModel' => Str::ucfirst($rMode),
                        'modelRelationMode'      => $rMode,
                        'modelRelationModel'     => $rModelName,
                        'modelRelationFk'        => $rFk,
                        'modelRelationLk'        => $rLk,
                        'modelRelationColumn'    => empty($rColumns) ? '' : "->select(['" . implode("', '", $rColumns) . "'])",
                    ]);
                    $controllerRelation[] = Str::camel($rTable);
                }
            }
            $modelRelationFieldAppends = empty($modelRelationFieldAppends) ? '' : $this->getReplacedTmpl('mixins/modelRelationFieldAppends', [
                'appendFields' => "'" . implode("', '", array_unique($modelRelationFieldAppends)) . "'",
            ]);
            $controllerRelation = empty($controllerRelation) ? '' : $this->getReplacedTmpl('mixins/controllerRelation', [
                'relationModel' => "'" . implode("', '", array_unique($controllerRelation)) . "'",
            ]);
        }

        //非覆盖模式时如果存在控制器文件则报错
        if (is_file($controllerFile) && !$force) {
            throw new Exception("控制器文件【{$controllerFile}】已存在，使用【-f】强制替换。");
        }

        //非覆盖模式时如果存在模型文件则报错
        if (is_file($modelFile) && !$force) {
            throw new Exception("模型文件【{$modelFile}】已存在，使用【-f】强制替换。");
        }

        $columnList = Db::connection($connection)
                        ->select("SELECT * FROM `information_schema`.`COLUMNS` WHERE TABLE_SCHEMA = ? AND table_name = ? ORDER BY ORDINAL_POSITION", [
                            Db::connection($connection)->getDatabaseName(),
                            $tablePrefix . $table,
                        ]);
        $fieldArr = [];
        $controllerFields = $controllerSearchFields = $modelFields = $modelProperty = [];
        foreach ($columnList as $v) {
            $fieldArr[] = $v->COLUMN_NAME;

            $comment = str_replace(["\r\n", "\r", "\n",], "    ", $v->COLUMN_COMMENT);

            $modelProperty[] = " * @property " . ($this->dataTypeList['mysql'][$v->DATA_TYPE] ?? $v->DATA_TYPE) . ($v->IS_NULLABLE === 'NO' ? '' : '|null') . " \${$v->COLUMN_NAME} {$comment}";

            $field = "        '{$v->COLUMN_NAME}', //{$comment}";
            if (!in_array($field, $this->deleteTimeDict) || !str_contains($field, 'password')) {
                $controllerFields[] = $field;
            }
            $searchType = match ($this->dataTypeList['mysql'][$v->DATA_TYPE] ?? $v->DATA_TYPE) {
                'string' => 'like',
                default => '='
            };
            if (strlen($comment) < 50) {
                $controllerSearchFields[] = "        '{$v->COLUMN_NAME}' => ['{$searchType}'], //{$comment}";
            } else {
                $controllerSearchFields[] = "        //{$comment}\n        '{$v->COLUMN_NAME}' => ['{$searchType}'],";
            }

            if (!in_array($v->COLUMN_NAME, array_merge(['id'], $this->createTimeDict, $this->updateTimeDict, $this->deleteTimeDict))) {
                $modelFields[] = $field;
            }
        }

        //软删除
        $deleteTime = array_intersect($this->deleteTimeDict, $fieldArr) ? current(array_intersect($this->deleteTimeDict, $fieldArr)) : null;
        if (!empty($deleteTime)) {
            $modelUse[] = 'use Illuminate\Database\Eloquent\SoftDeletes;';
        }

        sort($modelUse);
        sort($controllerUse);
        $data = [
            //模型
            'modelNamespace'            => $modelNamespace,
            'modelUse'                  => implode("\n", array_unique($modelUse)),
            'modelExtends'              => $modelExtends,
            'modelProperty'             => implode("\n", $modelProperty),
            'modelConnection'           => $connection ? "\n    protected \$connection = '$connection';\n" : '',
            'modelTrait'                => $deleteTime ? "\n    use SoftDeletes;\n" : '',
            'modelName'                 => $modelName,
            'modelTableName'            => $table,
            'modelTimeFieldType'        => version_compare(PHP_VERSION, '8.3', '>=') ? 'string ' : '',
            'createTime'                => array_intersect($this->createTimeDict, $fieldArr) ? '\'' . current(array_intersect($this->createTimeDict, $fieldArr)) . '\'' : 'null',
            'updateTime'                => array_intersect($this->updateTimeDict, $fieldArr) ? '\'' . current(array_intersect($this->updateTimeDict, $fieldArr)) . '\'' : 'null',
            'deleteTimeRow'             => $deleteTime ? "\n    const string DELETED_AT = '{$deleteTime}';" : '',
            // 'modelFields'    => "'" . implode("', '", $fieldArr) . "'",
            'modelFields'               => implode("\n", $modelFields),
            //控制器
            'controllerNamespace'       => $controllerNamespace,
            'controllerUse'             => implode("\n", array_unique($controllerUse)),
            'controllerExtends'         => $controllerExtends,
            'controllerName'            => $controllerName,
            'controllerFields'          => implode("\n", $controllerFields),
            'controllerSearchFields'    => implode("\n", $controllerSearchFields),
            //关联模型
            'modelRelationFieldAppends' => $modelRelationFieldAppends,
            'modelRelationField'        => implode("\n", $modelRelationField),
            'modelRelationTable'        => implode("\n", $modelRelationTable),
            'controllerRelation'        => $controllerRelation,
        ];

        // 生成控制器文件
        $this->writeToFile('controller', $data, $controllerFile);
        // 生成模型文件
        $this->writeToFile('model', $data, $modelFile);

        $output->writeln('Build Success');

        return self::SUCCESS;
    }

    /**
     * 写入到文件
     * @param string $name
     * @param array  $data
     * @param string $pathname
     * @return bool|int
     */
    protected function writeToFile(string $name, array $data, string $pathname): bool|int
    {
        foreach ($data as &$datum) {
            $datum = is_array($datum) ? '' : $datum;
        }
        unset($datum);
        $content = $this->getReplacedTmpl($name, $data);

        if (!is_dir(dirname($pathname))) {
            mkdir(dirname($pathname), 0755, true);
        }

        echo $pathname . "\n";

        return file_put_contents($pathname, $content);
    }

    /**
     * 获取替换后的数据
     * @param string $name
     * @param array  $data
     * @return string
     */
    protected function getReplacedTmpl(string $name, array $data): string
    {
        foreach ($data as $index => &$datum) {
            $datum = is_array($datum) ? '' : $datum;
        }
        unset($datum);
        $search = $replace = [];
        foreach ($data as $k => $v) {
            $search[] = "{%{$k}%}";
            $replace[] = $v;
        }
        $tmplName = $this->getTmpl($name);
        if (isset($this->tmplList[$tmplName])) {
            $tmpl = $this->tmplList[$tmplName];
        } else {
            $this->tmplList[$tmplName] = $tmpl = file_get_contents($tmplName);
        }

        return str_replace($search, $replace, $tmpl);
    }

    /**
     * 获取基础模板
     * @param string $name
     * @return string
     */
    protected function getTmpl(string $name): string
    {
        return __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'template' . DIRECTORY_SEPARATOR . config('plugin.hiders.webman-crud.app.model_type') . DIRECTORY_SEPARATOR . $name . '.tmpl';
    }

    /**
     * 获取控制器相关信息
     * @param string      $module
     * @param string      $table
     * @param string|null $controller
     * @return array
     * @throws \Exception
     */
    protected function getControllerData(string $module, string $table, string $controller = null): array
    {
        return $this->getParseNameData($module, $table, 'controller', $controller);
    }

    /**
     * 获取模型相关信息
     * @param string      $module
     * @param string      $table
     * @param string|null $model
     * @return array
     * @throws \Exception
     */
    protected function getModelData(string $module, string $table, string $model = null): array
    {
        return $this->getParseNameData($module, $table, 'model', $model);
    }

    /**
     * 获取已解析相关信息
     * @param string      $module 模块名称
     * @param string|null $name   自定义名称
     * @param string      $table  数据表名
     * @param string      $type   解析类型，本例中为controller、model、validate
     * @return array
     * @throws Exception
     */
    protected function getParseNameData(string $module, string $table, string $type, string $name = null): array
    {
        $name = str_replace(['.', '/', '\\', '__'], '_', Str::snake($name ?: $table));
        $arr = explode('_', $name);
        count($arr) == 1 && $arr[1] = $arr[0];
        $parseName = ucfirst(Str::camel(join('_', array_slice($arr, 1)))) . ($type === 'controller' ? config('app.controller_suffix') : '');
        $parseArr = $arr;
        $parseArr[] = $parseName;
        //类名不能为内部关键字
        if (in_array(strtolower($parseName), $this->internalKeywords)) {
            throw new Exception('不能使用内部关键字:' . $parseName);
        }
        $parseNamespace = str_replace('/', '\\', $module) . '\\' . $arr[0];
        $parseFile = $module . DIRECTORY_SEPARATOR . $arr[0] . DIRECTORY_SEPARATOR . $parseName . '.php';

        return [$parseNamespace, $parseName, $parseFile, $parseArr];
    }
}
